﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace CSharpExt.IO.Compression
{

    // ======================================================================
    // zlib
    // ======================================================================

    /// <summary>
    /// Questa classe ha il solo compito di mappare in ambiente .NET le API 
    /// messe a disposizione dalla libreria binaria "zlib1.dll" (nella sua
    /// versione a 32 o 64 bit)
    /// 
    /// deflate -> zip
    /// inflate -> unzip
    /// 
    /// </summary>
    class zlib
    {
        #region z_stream Constants
        // ==================================================================
        // z_stream Constants
        // ==================================================================

        internal static readonly int Z_NO_FLUSH = 0;
        internal static readonly int Z_PARTIAL_FLUSH = 1; /* will be removed, use Z_SYNC_FLUSH instead */
        internal static readonly int Z_SYNC_FLUSH = 2;
        internal static readonly int Z_FULL_FLUSH = 3;
        internal static readonly int Z_FINISH = 4;
        internal static readonly int Z_BLOCK = 5;

        /* Allowed flush values; see deflate() and inflate() below for details */

        internal static readonly int Z_OK = 0;
        internal static readonly int Z_STREAM_END = 1;
        internal static readonly int Z_NEED_DICT = 2;
        internal static readonly int Z_ERRNO = (-1);
        internal static readonly int Z_STREAM_ERROR = (-2);
        internal static readonly int Z_DATA_ERROR = (-3);
        internal static readonly int Z_MEM_ERROR = (-4);
        internal static readonly int Z_BUF_ERROR = (-5);
        internal static readonly int Z_VERSION_ERROR = (-6);

        /* 
         * Return codes for the compression/decompression functions. Negative
         * values are errors, positive values are used for special but normal events.
         */
        internal static readonly int Z_NO_COMPRESSION = 0;
        internal static readonly int Z_BEST_SPEED = 1;
        internal static readonly int Z_BEST_COMPRESSION = 9;
        internal static readonly int Z_DEFAULT_COMPRESSION = (-1);

        #endregion

        #region z_stream Data Structure
        // ==================================================================
        // z_stream Data Structure
        // ==================================================================
        [DllImport("zlib1.dll")]
        internal static extern unsafe byte* zlibVersion();

        #endregion

        #region High Level Zip Functions
        // ==================================================================
        // High Level Zip functions
        // ==================================================================

        [DllImport("zlib1.dll")]
        internal static unsafe extern int compress(byte* dest, int* destLen, byte* source, int sourceLen);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int compress2(byte* dest, int* destLen, byte* source, int sourceLen, int level);

        [DllImport("zlib1.dll")]
        internal static extern long compressBound(int sourceLen);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int uncompress(byte* dest, int* destLen, byte* source, int sourceLen);

        #endregion

        #region Low Level Zip functions: deflate (zip)
        // ==================================================================
        // Low Level Zip functions : deflate
        // ==================================================================
        /*
         * deflateInit(strm, level)  deflateInit_((strm), (level), zlibVersion(), sizeof(z_stream))
         */

        internal static unsafe int deflateInit(z_stream* strm, int level)
        {
            return deflateInit_(strm, level, zlibVersion(), sizeof(z_stream));
        }

        [DllImport("zlib1.dll")]
        internal static unsafe extern int deflateInit_(z_stream* strm, int level, byte* version, int stream_size);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int deflate(z_stream* strm, int flush);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int deflateEnd(z_stream* strm);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int deflateReset(z_stream* strm);

        #endregion

        #region Low Level Zip Functions : inflate (unzip)
        // ==================================================================
        // Low Level Zip functions : inflate
        // ==================================================================
        /*
         * inflateInit(strm)   inflateInit_((strm), zlibVersion(), sizeof(z_stream))
         */

        internal static unsafe int inflateInit(z_stream* strm)
        {
            return inflateInit_(strm, zlibVersion(), sizeof(z_stream));
        }

        [DllImport("zlib1.dll")]
        internal static unsafe extern int inflateInit_(z_stream* strm, byte* version, int stream_size);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int inflate(z_stream* strm, int flush);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int inflateEnd(z_stream* strm);

        [DllImport("zlib1.dll")]
        internal static unsafe extern int inflateReset(z_stream* strm);

        #endregion
    }

    // ======================================================================
    // z_stream
    // ======================================================================

    /// <summary>
    /// Questa struttura dati ha la stessa forma di quella utilizzata dalla
    /// libreria "zlib" (http://www.zlib.net/).
    /// 
    /// E' stata riprodotta qui per semplificare l'utilizzo delle funzioni
    /// a basso livello (deflate???, inflate???) messe a disposizione.
    /// 
    /// ATTENZIONE:
    /// - la libreria e' scritta in C e quindi le dichiarazioni delle strutture dati
    ///   sono state opportunamente convertite per assicurarsi che abbiano la stessa
    ///   dimensione anche in .NET
    /// - a seconda della piattaforma, i putatori (byte*) possono essere a 32 o 64bit!!!!
    /// - benche' in "zlib.h" alcuni membri della struttura siano dichiarati uLong,
    ///   (unsigned long) sulle piattaforme a 32 e 64bit un "long" ha la stessa 
    ///   dimensione di un "int" e cioe' 32bit
    /// - in .NET un "long" e' un intero a 64bit!!!
    /// 
    /// Inoltre a
    /// </summary>
    unsafe struct z_stream
    {
        #region Data

        public byte* next_in;
        public int/*uInt*/  avail_in;
        public int/*uLong*/ total_in;

        public byte* next_out;
        public int/*uInt*/  avail_out;
        public int/*uLong*/ total_out;

        public byte* msg;       // non sono interessante
        public byte* state;     // non sono interessante
        public byte* zalloc;    // non sono interessante
        public byte* zfree;     // non sono interessante
        public byte* opaque;    // non sono interessante

        public int data_type;
        public int/*uLong*/ adler;
        public int/*uLong*/ reserved;

        #endregion
    };

    // ======================================================================
    // ZlibStream
    // ======================================================================

    public enum CompressionLevel
    {
        #region Compression Levels
        // ==================================================================
        // Compression Levels
        // ==================================================================

        Level0,
        Level1,
        Level2,
        Level3,
        Level4,
        Level5,
        Level6,
        Level7,
        Level8,
        Level9,
        BestCompression = Level9,
        BestSpeed = Level1,
        None = Level0,
        Default = -1

        #endregion
    }

    public unsafe class ZlibStream : Stream
    {
        #region Private Data
        // ==================================================================
        // Private Data
        // ==================================================================

        private static readonly int CHUNK = 4096;

        private Stream _stream;
        private CompressionMode _mode;
        private CompressionLevel _level;
        private byte[] _buffer = new byte[CHUNK];
        private int _offset;
        private z_stream strm;
        private int _clength;
        private int _writed;

        #endregion

        #region Constructor
        // ==================================================================
        // Constructor
        // ==================================================================

        public ZlibStream(Stream stream)
            : this(stream, CompressionMode.Decompress)
        {

        }


        public ZlibStream(Stream stream, CompressionMode mode)
            : this(stream, mode, CompressionLevel.Default)
        {

        }

        public ZlibStream(Stream stream, CompressionLevel level)
            : this(stream, CompressionMode.Compress, level)
        {

        }


        public ZlibStream(Stream stream, CompressionMode mode, CompressionLevel level)
        {
            _stream = stream;
            _mode = mode;
            _level = level;

            if (mode == CompressionMode.Compress)
                deflateInit();
            else
                inflateInit();
        }

        #endregion

        #region Properties
        // ==================================================================
        // Properties
        // ==================================================================

        public override bool CanRead
        {
            get { return _mode == CompressionMode.Decompress; }
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override bool CanWrite
        {
            get { return _mode == CompressionMode.Compress; }
        }

        public int CompressedLength
        {
            get { return _clength; }
        }

        public override long Length
        {
            get
            {
                Debug.Assert(_mode == CompressionMode.Compress);

                return _writed;
            }
        }

        #endregion

        #region Operations
        // ==================================================================
        // Operations
        // ==================================================================

        public override void Write(byte[] buffer, int offset, int count)
        {
            Debug.Assert(_mode == CompressionMode.Compress);

            _writed += count;

            deflate(buffer, offset, count);
        }

        public override int  Read(byte[] buffer, int offset, int count)
        {
            Debug.Assert(_mode == CompressionMode.Decompress);

            return inflate(buffer, offset, count);
        }

        public override void Flush()
        {
            _stream.Flush();
        }

        public override void Close()
        {
            if (_mode == CompressionMode.Compress)
            {
                deflate(_buffer, 0, 0);
                deflateEnd();
            }
            else
            {
                inflateEnd();
            }

            _stream.Close();
            base.Close();
        }

        #endregion

        #region Implementation
        // ==================================================================
        // Implementation
        // ==================================================================

        private void deflateInit()
        {
            fixed (z_stream* pstrm = &strm)
            {
                int ret = zlib.deflateInit(pstrm, (int)_level);
                if (ret != zlib.Z_OK)
                    throw new System.ArgumentException(ret.ToString());
            }
        }

        private void deflate(byte[] buffer, int offset, int count)
        {
            fixed (byte* pin = &buffer[offset])
            fixed (byte* pout = &_buffer[0])
            fixed (z_stream* pstrm = &strm)
            {
                strm.next_in = pin;
                strm.avail_in = count;

                int finish = count == 0 ? zlib.Z_FINISH : zlib.Z_NO_FLUSH;

                do
                {
                    strm.next_out = pout;
                    strm.avail_out = CHUNK;

                    int ret = zlib.deflate(pstrm, finish);

                    int ocount = CHUNK - strm.avail_out;
                    if (ocount != 0)
                    {
                        _stream.Write(_buffer, 0, ocount);
                        _clength += ocount;
                    }

                } while (strm.avail_out == 0);
            }
        }

        private void deflateEnd()
        {
            fixed (z_stream* pstrm = &strm)
            {
                int ret = zlib.deflateEnd(pstrm);
                if (ret != zlib.Z_OK)
                    throw new System.ArgumentException(ret.ToString());
            }
        }


        private void inflateInit()
        {
            fixed (z_stream* pstrm = &strm)
            {
                int ret = zlib.inflateInit(pstrm);
                if (ret != zlib.Z_OK)
                    throw new System.ArgumentException(ret.ToString());
            }
        }

        private int inflate(byte[] buffer, int offset, int count)
        {
            if (strm.avail_in == -1)
                return 0;

            int bytes = 0;

        read:
            if (strm.avail_in == 0)
            {
                _offset = 0;
                strm.avail_in = _stream.Read(_buffer, _offset, CHUNK);
            }

            if (count > 0)
            {
                fixed (z_stream* pstrm = &strm)
                fixed (byte* pat = &_buffer[0])
                fixed (byte* pin = &_buffer[_offset])
                fixed (byte* pout = &buffer[offset])
                {
                    strm.next_in = pin;
                    strm.next_out = pout;
                    strm.avail_out = count;

                    int ret = zlib.inflate(pstrm, zlib.Z_NO_FLUSH);
                    if (ret == zlib.Z_STREAM_END)
                    {
                        strm.avail_in = -1;
                    }

                    int used = count - strm.avail_out;
                    count -= used;
                    offset += used;
                    bytes += used;

                    if (strm.avail_out != 0 && ret == zlib.Z_OK)
                    {
                        goto read;
                    }

                    _offset = (int)(strm.next_in - pat);
                }
            }

            return bytes;
        }

        private void inflateEnd()
        {
            fixed (z_stream* pstrm = &strm)
            {
                int ret = zlib.inflateEnd(pstrm);
                if (ret != zlib.Z_OK)
                    throw new System.ArgumentException(ret.ToString());
            }
        }

        #endregion

        #region Unsupported Operations
        // ==================================================================
        // Unsupported Operations
        // ==================================================================

        public override long Position
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }

        #endregion
    }

    // ======================================================================
    // ZLibWrapper
    // ======================================================================

    /// <summary>
    /// La classe serve per zippare/unzippare dei buffer.
    /// Se deve comprimere o decomprimere deve essere deciso in fase di creazione
    /// dell'istanza.
    /// 
    /// In particolare, in fase di compressione si deve decidere il livello di 
    /// compressione. Possibili valori sono 0..9 e -1 che corrispondena 6.
    /// </summary>
    public class ZLibWrapper : IDisposable
    {
        #region Private Data
        // ==================================================================
        // Private Data
        // ==================================================================

        private z_stream _strm;

        private CompressionMode _mode;

        #endregion

        #region Constructor
        // ==================================================================
        // Constructor
        // ==================================================================

        /// <summary>
        /// Usare per zippare
        /// </summary>
        /// <param name="level">0..9 oppure -1</param>
        public ZLibWrapper(int level)
        {
            _mode = CompressionMode.Compress;
            unsafe
            {
                fixed (z_stream* pstrm = &_strm)
                {
                    int ret = zlib.deflateInit(pstrm, /*Z_DEFAULT_COMPRESSION*/level);
                    if (ret != 0)
                        throw new ApplicationException(String.Format("Zipper.<init>() failed: {0}", ret));
                }
            }
        }

        /// <summary>
        /// Usare per unzippare
        /// </summary>
        public ZLibWrapper()
        {
            _mode = CompressionMode.Decompress;
            unsafe
            {
                fixed (z_stream* pstrm = &_strm)
                {
                    int ret = zlib.inflateInit(pstrm);
                    if (ret != 0)
                        throw new ApplicationException(String.Format("Zipper.<init>() failed: {0}", ret));
                }
            }
        }

        #endregion

        #region Interface
        // ==================================================================
        // Interface
        // ==================================================================

        public int Compress(byte[] source, int sourceOffset, int sourceLength, byte[] dest, int destOffset)
        {
            Debug.Assert(_mode == CompressionMode.Compress);

            int destLength;

            unsafe
            {
                fixed (z_stream* pstrm = &_strm)
                fixed (byte* psource = &source[sourceOffset])
                fixed (byte* pdest = &dest[destOffset])
                {
                    _strm.next_in = psource;
                    _strm.avail_in = sourceLength;
                    _strm.next_out = pdest;
                    _strm.avail_out = dest.Length - destOffset;
                    int ret = zlib.deflate(pstrm, zlib.Z_FINISH);

                    destLength = _strm.total_out;

                    zlib.deflateReset(pstrm);
                    if (ret != 0 && ret != 1)
                        throw new ApplicationException("Zipper.Zip failed: " + ret);
                }
            }

            return destLength;
        }

        public int Decompress(byte[] source, int sourceOffset, int sourceLength, byte[] dest, int destOffset)
        {
            Debug.Assert(_mode == CompressionMode.Decompress);

            int destLength;

            unsafe
            {
                fixed (z_stream* pstrm = &_strm)
                fixed (byte* psource = &source[sourceOffset])
                fixed (byte* pdest = &dest[destOffset])
                {
                    _strm.next_in = psource;
                    _strm.avail_in = sourceLength;
                    _strm.next_out = pdest;
                    _strm.avail_out = dest.Length - destOffset;
                    _strm.total_out = 0;
                    int ret = zlib.inflate(pstrm, zlib.Z_FINISH);

                    destLength = _strm.total_out;

                    zlib.inflateReset(pstrm);
                    if (ret != 0 && ret != 1)
                        throw new ApplicationException("Unzipper.Unzip failed: " + ret);
                }
            }

            return destLength;
        }

        public void Dispose()
        {
            unsafe
            {
                fixed (z_stream* pstrm = &_strm)
                {
                    if (_mode == CompressionMode.Compress)
                    {
                        zlib.deflateEnd(pstrm);
                    }
                    else
                    {
                        zlib.inflateEnd(pstrm);
                    }
                }
            }
        }

        #endregion
    }

}
